package com.lyf.scm.plan.domain.repository;

import cn.hutool.core.util.ObjectUtil;
import com.lyf.scm.common.constants.BigDataEsConstants;
import com.lyf.scm.common.constants.EsAggrFunTypeConstants;
import com.lyf.scm.common.enums.*;
import com.lyf.scm.common.model.SalesPlanSelectDTO;
import com.lyf.scm.plan.api.dto.*;
import com.lyf.scm.plan.domain.remote.basedata.dto.ChannelDTO;
import com.lyf.scm.plan.domain.remote.basedata.dto.StoreDTO;
import com.lyf.scm.plan.domain.remote.item.dto.SkuUnitDTO;
import com.lyf.scm.plan.domain.service.RedisCacheService;
import com.lyf.scm.plan.insfrastructure.db.dataobject.es.SalesPlanEsEntity;
import com.rome.arch.core.exception.RomeException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation;
import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder;
import org.elasticsearch.search.aggregations.bucket.composite.TermsValuesSourceBuilder;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

import static com.lyf.scm.common.constants.CommonConstants.CATEGORY_LEVEL_FOUR;
import static com.lyf.scm.common.constants.CommonConstants.DEFAULT_PULLED_SIZE;
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;

/**
 * es懒加载查询解决方案
 *
 * @author wangchlei
 * @date 2020/11/14
 */
@Service
public class SalesPlanEsLazyLoadingRepository {
    @Resource
    private ElasticsearchTemplate elasticsearchTemplate;
    @Resource
    private RedisCacheService redisCacheService;

    /**
     * 先从es中过滤出该条件下所有的存在的sku（去重）
     *
     * @param esAggregation
     * @return {@link Set<String>}
     **/
    public Set<String> queryAllSku(EsAggregationDTO esAggregation) {
        //获取QueryBuilder
        BoolQueryBuilder queryBuilder = getBoolQueryBuilder(esAggregation.getEsQuery());
        //获取AggregationBuilders
        TermsAggregationBuilder groupSku = AggregationBuilders.terms(BigDataEsConstants.GROUP_SKU)
                .field(EsConmulEnum.SKU_KEYWORD.getFName()).size(Integer.MAX_VALUE).shardSize(Integer.MAX_VALUE);
        SearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(queryBuilder)
                .withIndices(BigDataEsConstants.INDEX_NAME_PLAN_SALES_PLAN)
                .addAggregation(groupSku)
                .withPageable(PageRequest.of(0, 1))
                .build();
        AggregatedPage<SalesPlanEsEntity> esEntities = elasticsearchTemplate.queryForPage(query, SalesPlanEsEntity.class);
        if (esEntities.getTotalElements() == 0) {
            return null;
        }
        // 解析es返回结果
        return parsingData(esEntities);
    }

    public EsScrollReturnDTO pullDataList(String sku, EsAggregationDTO esAggregation, String afterKey) {
        //重置sku过滤条件
        this.resetCateInfo(sku, esAggregation);
        AggregatedPage<SalesPlanEsEntity> salesPlanEsEntities = this.scrollQueryByGroup(esAggregation, afterKey);
        if (salesPlanEsEntities == null) {
            return new EsScrollReturnDTO();
        }
        // 解析es返回结果
        EsScrollReturnDTO esScrollReturnDTO = parsingScrollData(salesPlanEsEntities, esAggregation);
        return esScrollReturnDTO;
    }

    /**
     * 换算比例
     *
     * @param paramList
     * @param unitType
     * @return List<EsReturnDTO>
     * @author heray
     * @date 2020/11/13
     **/
    public List<EsReturnDTO> conversionUnit(List<EsReturnDTO> paramList, Long unitType) {
        List<String> skuCodeList = paramList.stream().filter(e -> StringUtils.isNotEmpty(e.getSkuCode()))
                .map(EsReturnDTO::getSkuCode).distinct().collect(Collectors.toList());
        if (CollectionUtils.isEmpty(skuCodeList) || unitType == null) {
            return paramList;
        }
        Long baseUnit = SkuUnitTypeEnum.BASIS_UNIT.getUnitType();
        if (!unitType.equals(baseUnit)) {
            List<UnitRateQueryDTO> unitQueryList = skuCodeList.stream().map(sku -> {
                UnitRateQueryDTO unit = new UnitRateQueryDTO();
                unit.setSkuCode(sku);
                unit.setFromUnitType(baseUnit);
                unit.setToUnitType(unitType);
                return unit;
            }).collect(Collectors.toList());
            Map<String, SkuUnitDTO> unitRatio = redisCacheService.getUnitConversionRatio(unitQueryList);
            final int tLength = 3;
            return paramList.stream().map(val -> {
                SkuUnitDTO unitInfo = unitRatio.get(val.getSkuCode() + baseUnit + unitType);
                if (ObjectUtils.isEmpty(unitInfo)) {
                    return val;
                }
                val.setUnitCode(unitInfo.getUnitCode());
                val.setUnitName(unitInfo.getUnitName());
                BigDecimal scale = BigDecimal.valueOf(unitInfo.getScale());
                Map<String, List<BigDecimal>> targetMap = val.getTargetMap();
                Map<String, List<BigDecimal>> newTargetMap = new HashMap<>();
                for (String key : targetMap.keySet()) {
                    List<BigDecimal> newTargetList = new ArrayList<>();
                    List<String> notConversionTarget = EsSumColumnEnum.getNotConversionTarget();
                    if (notConversionTarget.contains(key)) {
                        newTargetMap.put(key, targetMap.get(key));
                        continue;
                    }
                    targetMap.get(key).forEach(v -> {
                        BigDecimal newTarget = v.multiply(scale);
                        newTarget = newTarget.scale() > tLength ? newTarget.setScale(tLength, BigDecimal.ROUND_HALF_UP) : newTarget;
                        newTargetList.add(newTarget);
                    });
                    newTargetMap.put(key, newTargetList);
                }
                val.setTargetMap(newTargetMap);
                return val;
            }).collect(Collectors.toList());
        }
        return paramList;
    }

    /**
     * 解析ES返回结果
     *
     * @param entities
     * @param esAggregation
     * @return
     */
    private EsScrollReturnDTO parsingScrollData(AggregatedPage<SalesPlanEsEntity> entities, EsAggregationDTO esAggregation) {
        EsScrollReturnDTO esScrollReturnDTO = new EsScrollReturnDTO();
        List<String> groupColumns = esAggregation.getGroupColumns();
        List<String> sumColumns = esAggregation.getSumColumns();
        CompositeAggregation parsedComposite = entities.getAggregations().get(BigDataEsConstants.GROUP_PRI);
        if (parsedComposite.afterKey() == null) {
            return esScrollReturnDTO;
        }
        esScrollReturnDTO.setAfterKey(parsedComposite.afterKey().toString());
        List<? extends CompositeAggregation.Bucket> list = parsedComposite.getBuckets();
        List<EsReturnDTO> esReturnDTOList = new ArrayList<>();
        EsReturnDTO esReturn;
        for (CompositeAggregation.Bucket parsedBucket : list) {
            // 初始化返回参数
            esReturn = new EsReturnDTO();
            // 解析列
            analyzeColumns(esReturn, groupColumns, parsedBucket);
            // 解析指标
            Map<String, List<BigDecimal>> targetMap =
                    analyzeTarget((StringTerms) parsedBucket.getAggregations().asMap().get(BigDataEsConstants.GROUP_DATE), sumColumns, esAggregation.getDateList());
            esReturn.setTargetMap(targetMap);
            esReturnDTOList.add(esReturn);
        }
        //小于该查询出的size，则判定接下来不可以再使用该afterKey查询
        if (list.size() < DEFAULT_PULLED_SIZE) {
            esScrollReturnDTO.setAfterKey(null);
        }
        esScrollReturnDTO.setEsReturnDTOList(esReturnDTOList);
        return esScrollReturnDTO;
    }

    private void analyzeColumns(EsReturnDTO esReturn, List<String> groupColumns, CompositeAggregation.Bucket parsedBucket) {
        // 解析key
        String keyAsString = parsedBucket.getKey().get(BigDataEsConstants.GROUP_MULTI).toString();
        String[] split = keyAsString.split("&&");
        int length = split.length;
        for (int i = 0; i < length; i++) {
            if (length == 1) {
                setColumns(esReturn, groupColumns.get(i), split[i]);
            } else {
                setColumns(esReturn, groupColumns.get(i), split[i].substring(1, split[i].length() - 1));
            }
        }
    }

    private void setColumns(EsReturnDTO info, String column, String target) {
        if (column.equals(EsConmulEnum.CHANNEL_KEYWORD.getFName())) {
            ChannelDTO channelInfo = redisCacheService.getAllChannelInfo().get(target);
            info.setChannel(target);
            info.setChannelName(channelInfo.getChannelName());
            return;
        }
        if (column.equals(EsConmulEnum.PROVINCE_CODE_KEYWORD.getFName())) {
            getAreaInfo(info, target, 1);
            return;
        }
        if (column.equals(EsConmulEnum.CITY_CODE_KEYWORD.getFName())) {
            getAreaInfo(info, target, 1);
            return;
        }
        if (column.equals(EsConmulEnum.COUNTY_CODE_KEYWORD.getFName())) {
            getAreaInfo(info, target, 1);
            return;
        }
        if (column.equals(EsConmulEnum.STORE_KEYWORD.getFName())) {
            getAreaInfo(info, target, 2);
            return;
        }
        if (column.equals(EsConmulEnum.WAREHOUSE_KEYWORD.getFName())) {
            StoreDTO warehouseInfo = redisCacheService.getDeliverWarehouse().get(target);
            info.setDeliverName(warehouseInfo.getName());
            info.setDeliverId(target);
            return;
        }
        if (column.equals(EsConmulEnum.PLAN_TYPE.getFName())) {
            info.setPlanType(target);
            info.setPlanTypeName(SalesOtherTypeEnum.getNameByType(target));
            return;
        }
        if (column.equals(EsConmulEnum.SKU_KEYWORD.getFName())) {
            BaseSkuInfoDTO baseSkuInfo =
                    redisCacheService.getCategoryInfoBySkuCodes(Collections.singletonList(target)).get(target);
            info.setCategory(baseSkuInfo.getCategory());
            info.setSkuCode(baseSkuInfo.getSkuCode());
            info.setSkuName(baseSkuInfo.getSkuName());
        }
    }

    private Map<String, List<BigDecimal>> analyzeTarget(StringTerms dateStringTerms, List<String> sumColumns, List<String> dateList) {
        // 小数位保留长度为3
        final int tLength = 3;
        Map<String, List<BigDecimal>> targetMap = new HashMap<>();
        for (String column : sumColumns) {
            // 返回在每个时间内的同一个指标
            Map<String, BigDecimal> sumMap = dateStringTerms.getBuckets().stream()
                    .collect(Collectors.toMap(StringTerms.Bucket::getKeyAsString,
                            sum -> {
                                NumericMetricsAggregation.SingleValue aggregation = sum.getAggregations().get(column);
                                return Double.isNaN(aggregation.value()) ? BigDecimal.ZERO : new BigDecimal(aggregation.getValueAsString());
                            }));
            // 根据时间排序add到list中
            List<BigDecimal> targetList = new ArrayList<>();
            dateList.forEach(date -> {
                BigDecimal target = sumMap.keySet().stream().filter(date::equals)
                        .map(sumMap::get).findFirst().orElse(BigDecimal.ZERO);
                target = target.scale() > tLength ? target.setScale(tLength, BigDecimal.ROUND_HALF_UP) : target;
                targetList.add(target);
            });
            targetMap.put(column, targetList);
        }
        return targetMap;
    }

    private void getAreaInfo(EsReturnDTO info, String area, Integer type) {
        if (StringUtils.isEmpty(area)) {
            return;
        }
        BaseAreaInfoDTO areaInfo = redisCacheService.getAreaInfo(Collections.singletonList(area), type).get(area);
        if (ObjectUtil.isEmpty(areaInfo)) {
            return;
        }
        info.setArea(areaInfo.getAreaNameSplicing());
        info.setAreaCode(areaInfo.getAreaCodeSplicing());
    }

    /**
     * 获取结果集
     *
     * @param esAggregation
     * @param afterKey
     * @return {@link AggregatedPage<SalesPlanEsEntity>}
     **/
    public AggregatedPage<SalesPlanEsEntity> scrollQueryByGroup(EsAggregationDTO esAggregation, String afterKey) {
        //获取QueryBuilder
        BoolQueryBuilder queryBuilder = getBoolQueryBuilder(esAggregation.getEsQuery());
        //获取AggregationBuilders
        CompositeAggregationBuilder comAgg;
        if (CollectionUtils.isNotEmpty(esAggregation.getGroupColumns())) {
            comAgg = getScrollGroupConditions(esAggregation.getGroupColumns());
            Map<String, Object> afterKeyMap = new HashMap<>();
            afterKeyMap.put(BigDataEsConstants.GROUP_MULTI, afterKey);
            comAgg.aggregateAfter(afterKeyMap);
        } else {
            throw new RomeException("999", "分组列至少存在一组");
        }
        TermsAggregationBuilder groupDate = AggregationBuilders.terms(BigDataEsConstants.GROUP_DATE)
                .field(EsConmulEnum.DATE_KEYWORD.getFName()).size(Integer.MAX_VALUE).shardSize(Integer.MAX_VALUE);
        //获取SumAggregationBuilders
        List<EsSumColumnEnum> sumColumnList = EsSumColumnEnum.getEsSumColumnList(esAggregation.getSumColumns());
        getSumConditions(sumColumnList).forEach(groupDate::subAggregation);
        comAgg.subAggregation(groupDate);
        AggregatedPage<SalesPlanEsEntity> entities = scrollAggregationQuery(queryBuilder, comAgg);
        if (entities.getTotalElements() == 0) {
            return null;
        }
        return entities;
    }

    public AggregatedPage<SalesPlanEsEntity> scrollAggregationQuery(BoolQueryBuilder queryBuilder, CompositeAggregationBuilder comAgg) {
        SearchQuery query = new NativeSearchQueryBuilder().withPageable(PageRequest.of(0, 1))
                .withQuery(queryBuilder)
                .withIndices(BigDataEsConstants.INDEX_NAME_PLAN_SALES_PLAN)
                .addAggregation(comAgg)
                .build();
        return elasticsearchTemplate.queryForPage(query, SalesPlanEsEntity.class);
    }

    /**
     * 解析聚合类型
     *
     * @param sumColumns
     * @return
     */
    private List<AggregationBuilder> getSumConditions(List<EsSumColumnEnum> sumColumns) {
        // 指标sum聚合
        List<AggregationBuilder> sumList = new ArrayList<>();
        for (EsSumColumnEnum sumColumn : sumColumns) {
            switch (sumColumn.getType()) {
                case EsAggrFunTypeConstants.SUM:
                    sumList.add(AggregationBuilders.sum(sumColumn.getColumn()).script(new Script(sumColumn.getFormula())));
                    break;
                case EsAggrFunTypeConstants.MAX:
                    sumList.add(AggregationBuilders.max(sumColumn.getColumn()).script(new Script(sumColumn.getFormula())));
                    break;
                case EsAggrFunTypeConstants.MIN:
                    sumList.add(AggregationBuilders.min(sumColumn.getColumn()).script(new Script(sumColumn.getFormula())));
                    break;
                case EsAggrFunTypeConstants.AVG:
                    sumList.add(AggregationBuilders.avg(sumColumn.getColumn()).script(new Script(sumColumn.getFormula())));
                    break;
                case EsAggrFunTypeConstants.COUNT:
                    sumList.add(AggregationBuilders.count(sumColumn.getColumn()).script(new Script(sumColumn.getFormula())));
                    break;
                case EsAggrFunTypeConstants.CARDINALITY:
                    sumList.add(AggregationBuilders.cardinality(sumColumn.getColumn()).script(new Script(sumColumn.getFormula())));
                    break;
                default:
                    break;
            }
        }
        return sumList;
    }

    /**
     * 获取分组参数
     *
     * @param groupColumns
     * @return
     */
    private CompositeAggregationBuilder getScrollGroupConditions(List<String> groupColumns) {
        String grammar = "doc['&'].values";
        String spiltStr = "";
        StringBuilder scriptStr = new StringBuilder();
        for (String groupColumn : groupColumns) {
            String newGrammar = grammar.replace("&", groupColumn);
            scriptStr.append(spiltStr).append(newGrammar);
            spiltStr = "+'&&'+";
        }
        Script script = new Script(scriptStr.toString());
        TermsValuesSourceBuilder dataBuilder = new TermsValuesSourceBuilder(BigDataEsConstants.GROUP_MULTI).script(script);
        List<CompositeValuesSourceBuilder<?>> sources = new ArrayList<>();
        sources.add(dataBuilder);
        return new CompositeAggregationBuilder(BigDataEsConstants.GROUP_PRI, sources).size(DEFAULT_PULLED_SIZE);
    }

    private void resetCateInfo(String sku, EsAggregationDTO esAggregation) {
        List<SalesPlanSelectDTO> categoryCodeList = new ArrayList<>();
        SalesPlanSelectDTO salesPlanSelectDTO = new SalesPlanSelectDTO();
        salesPlanSelectDTO.setLevel(CATEGORY_LEVEL_FOUR);
        salesPlanSelectDTO.setCode(sku);
        categoryCodeList.add(salesPlanSelectDTO);
        esAggregation.getEsQuery().setCategoryCodeList(categoryCodeList);
    }

    /**
     * 解析ES返回结果
     *
     * @param entities
     * @return
     */
    private Set<String> parsingData(AggregatedPage<SalesPlanEsEntity> entities) {
        Set<String> set = new HashSet<>();
        StringTerms stringTerms = (StringTerms) entities.getAggregation(BigDataEsConstants.GROUP_SKU);
        List<StringTerms.Bucket> buckets = stringTerms.getBuckets();
        // 解析
        String skuCode;
        for (StringTerms.Bucket item : buckets) {
            skuCode = item.getKey().toString();
            if(skuCode != null){
                set.add(skuCode);
            }
        }
        return set;
    }

    /**
     * 获取查询条件
     *
     * @param baseSearch
     * @return
     */
    private BoolQueryBuilder getBoolQueryBuilder(EsQueryDTO baseSearch) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        queryBuilder.filter(rangeQuery(EsConmulEnum.DATE_KEYWORD.getFName()).gte(baseSearch.getStartDate())
                .lte(baseSearch.getEndDate()));
        //已取消的活动计划不展示
        if (CollectionUtils.isNotEmpty(baseSearch.getTypeList()) && !baseSearch.getTypeList().contains(SalesOtherTypeEnum.PLAN_DIRECTLY.getType())) {
            queryBuilder.mustNot(termQuery(EsConmulEnum.CANCEL_KEYWORD.getFName(), SalesCancelStatusEnum.ALREADYCANCEL.getType()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getTypeList())) {
            queryBuilder.filter(getShouldQuery(EsConmulEnum.PLAN_TYPE.getFName(), baseSearch.getTypeList()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getWarehouseCodeList())) {
            queryBuilder.filter(getShouldQuery(EsConmulEnum.WAREHOUSE_KEYWORD.getFName(), baseSearch.getWarehouseCodeList()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getLayeredCodeList())) {
            queryBuilder.filter(getShouldQuery(EsConmulEnum.LEVEL_KEYWORD.getFName(), baseSearch.getLayeredCodeList()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getChannelCodeList())) {
            queryBuilder.filter(getShouldQuery(EsConmulEnum.CHANNEL_KEYWORD.getFName(), baseSearch.getChannelCodeList()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getPermissionChannelList())) {
            queryBuilder.filter(getShouldQuery(EsConmulEnum.CHANNEL_KEYWORD.getFName(), baseSearch.getPermissionChannelList()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getQueryAreaCodeList())) {
            queryBuilder.filter(getAreaQuery(baseSearch.getQueryAreaCodeList()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getPermissionAreaList())) {
            queryBuilder.filter(getAreaQuery(baseSearch.getPermissionAreaList()));
        }
        if (CollectionUtils.isNotEmpty(baseSearch.getCategoryCodeList())) {
            queryBuilder.filter(getCateQuery(baseSearch.getCategoryCodeList()));
        }
        return queryBuilder;
    }

    private QueryBuilder getShouldQuery(String key, List<String> codeList) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        codeList.forEach(code -> queryBuilder.should(termQuery(key, code)));
        return queryBuilder;
    }

    private QueryBuilder getAreaQuery(List<SalesPlanSelectDTO> areaCodeList) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        areaCodeList.forEach(val -> {
            switch (val.getLevel()) {
                case 1:
                    queryBuilder.should(termQuery(EsConmulEnum.PROVINCE_CODE_KEYWORD.getFName(), val.getCode()));
                    break;
                case 2:
                    queryBuilder.should(termQuery(EsConmulEnum.CITY_CODE_KEYWORD.getFName(), val.getCode()));
                    break;
                case 3:
                    queryBuilder.should(termQuery(EsConmulEnum.COUNTY_CODE_KEYWORD.getFName(), val.getCode()));
                    break;
                case 4:
                    queryBuilder.should(termQuery(EsConmulEnum.STORE_KEYWORD.getFName(), val.getCode()));
                    break;
                default:
                    throw new RomeException(ResponseMsg.PARAM_ERROR.getCode(), ResponseMsg.PARAM_ERROR.getMsg());
            }
        });
        return queryBuilder;
    }

    private QueryBuilder getCateQuery(List<SalesPlanSelectDTO> categoryCodeList) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        categoryCodeList.forEach(val -> {
            switch (val.getLevel()) {
                case 2:
                    queryBuilder.should(termQuery(EsConmulEnum.PRI_KEYWORD.getFName(), val.getCode()));
                    break;
                case 3:
                    queryBuilder.should(termQuery(EsConmulEnum.SEC_KEYWORD.getFName(), val.getCode()));
                    break;
                case 4:
                    queryBuilder.should(termQuery(EsConmulEnum.TER_KEYWORD.getFName(), val.getCode()));
                    break;
                case 5:
                    queryBuilder.should(termQuery(EsConmulEnum.SKU_KEYWORD.getFName(), val.getCode()));
                    break;
                default:
                    throw new RomeException(ResponseMsg.PARAM_ERROR.getCode(), ResponseMsg.PARAM_ERROR.getMsg());
            }
        });
        return queryBuilder;
    }

}
